백엔드 서버 도커라이징, EC2 배포하는 과정 기록
서버 도커라이징
Production용 Dockerfile 작성
베이스 이미지 선정
- 최대한 가벼운 이미지
- 최소한의 필요한 것만 설치된 이미지
FROM python:3.10-slim
환경변수 설정
- 불필요한 파일 생성 방지
- 디버깅 설정
- 캐시 설정 안함
- 자주 쓰는 변수 설정
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=on \
APP_HOME=/app
PYTHONDONTWRITEBYTECODE=1
: python이 .pyc파일을 생성하지 않도록 함
PYTHONUNBUFFERED=1
: 표준 출력(stdout, stderr)를 버퍼링하지 않고 즉시 터미널 출력 - 앱이 crash될 때 마지막 로그가 버퍼에 갇히지 않도록 설정해서 디버깅에 유리
PIP_NO_CACHE_DIR=on
: pip 설치 시 캐시 디렉토리를 사용하지 않아 용량을 줄임
OS 환경 세팅
- 사용할 라이브러리 설치
- 폴더 생성
- 파일 권한 변경
- 캐시 삭제 등
설치할 때 하나 수정하고 빌드해보고, 이런식으로 하지 않고 테스트용 컨테이너를 하나 만들어서 실행해보는게 좋음.
기본 베이스 이미지만 가지고 컨테이너 하나 생성
$ docker build -t test-server -f <dockerfile 경로> .
$ docker run -it test-server /bin/bash
테스트할 때 호스트 파일 시스템에서 사용할 폴더나 환경변수가 있다면 docker run 시 -v로 볼륨을 연결하거나, -e 혹은 --env-file 옵션으로 환경변수를 추가해서 실행해도 된다.
이후 하나하나 직접 설치를 진행해보며 dockerfile에 내용을 추가해주기.
RUN apt-get update && \
apt-get install -y curl ca-certificates gnupg && \
install -m 0755 -d /etc/apt/keyrings && \
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg && \
chmod a+r /etc/apt/keyrings/docker.gpg && \
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list && \
apt-get update && \
apt-get install -y docker-ce-cli && \
apt-get clean && rm -rf /var/lib/apt/lists/*
현재 이 프로젝트에서는 컨테이너 내부에서 docker를 사용하기 위해 docker cli를 설치함.
이 때, docker engine은 호스트의 docker를 빌려 사용할 것이므로 전체가 아닌 cli만 설치했음.
의존성 설치
- 소스코드를 복사하기 전에, 우선적으로 의존성을 설치하기: 의존성이 기존과 같다면 docker의 layer cache로 인해 빌드 속도 상승
- 의존성 설치 후 필요한 폴더를 복사: 이 때, 개발환경과 달리, 실제 소스코들을 직접 복사해줘야함. 개발용 dockerfile에서는 hot reload를 위해 app 폴더를 volume으로 연결해서 사용했는데, production에서는 전부 컨테이너 내부로 옮겨서 실행
# Install Pipenv
RUN pip install --no-cache-dir pipenv # 설치 과정 중 임시파일 제거 옵션 추가
COPY Pipfile Pipfile.lock ./
# install dependency
RUN pipenv install --system --deploy # system옵션으로 가상환경 따로 안만들고 시스템에 직접 설치, deploy 옵션으로 `Pipfile.lock` 파일과 실제 설치된 패키지 상태가 하나라도 다르면 빌드를 실패하게 만듦.
COPY ./app ./app
COPY ./docker-file ./docker-file
COPY ./docker-env ./docker-env
주의사항: 이 때, 환경변수를 같이 COPY해서 넣으면 이미지를 가진 사람 모두 환경변수를 읽을 수 있게 됨. 따라서 중요한 환경변수는 추후 따로 서버로 옮겨서 외부에서 주입하는 방식이 보안상 유리. 현재 docker-env는 단순 변수 위주라 COPY해서 사용
work directory 설정
- 컨테이너가 뜬 뒤, 명령어를 실행할 위치를 지정
WORKDIR $APP_HOME
(옵션) docker run 시 실행할 명령어 입력
- docker compose 내에서 설정하면 해당 내용은 덮어써짐
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
Production용 Docker compose 파일 추가
- docker run을 통해 해도 되지만, 환경변수 추가나 명령어 실행 등을 쉽게 하기 위해 compose 도입
%% docker-compose.production.yml %%
services:
api-server:
image: ka1dyn/ecode-backend:latest
env_file:
- ./docker-env/docker.env.local
- ./docker-env/docker.env.user
ports:
- "8000:8000"
volumes:
- user-volumes:/user-data
- /var/run/docker.sock:/var/run/docker.sock
command: >
bash -c
"
echo '@PROD: pulling user-container image from registry...'
&& docker pull ka1dyn/user-container
&& docker tag ka1dyn/user-container user-container
&& pwd
&& uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
"
volumes:
user-volumes:
name: user-volumes
로컬에서 빌드, 실행 테스트
$ docker build -t ecode-backend -f docker-file/Dockerfile.production .
$ docker tag ecode-backend ka1dyn/ecode-backend <--- dockerhub push를 위해 계정이름 추가
$ docker-compose -f docker-compose.production.yml up -d

이 때, tag로 생성한 이미지는 기존 이미지와 ID가 똑같아서 추가적인 공간을 차지하지 않는다.
실행한 뒤, docker ps 명령어로 컨테이너가 잘 떴는지 확인

에러가 나면 위처럼 아무 컨테이너가 뜨지 않고, log를 확인해봐야한다.
로그 확인 방법
docker compose 혹은 컨테이너 name 기반 로그 확인 방법은 다음과 같다.
$ docker-compose -f docker-compose.production.yml logs
$ docker logs -f <컨테이너 name>
확인 결과, 현재 컨테이너 내부에서 user-container 이미지를 pull해오는데, 해당하는 이미지가 없어서 실행이 중지되었다. 따라서 로컬 테스트 시에는 pull을 생략하고, 직접 이미지를 build해두는 방식으로 테스트를 진행했다.
docker compose의 command 부분을 다음과 같이 수정하고 테스트했다.
%% docker-compose.production.yml %%
...
command: >
bash -c
"
echo '@PROD: pulling user-container image from registry...'
&& pwd
&& uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
"
...

잘 실행되며, frontend에서도 요청이 잘 되는 것을 확인했다.
Docker hub로 이미지 올리기
- AWS ECR을 사용할 수 있지만, 개인 프로젝트이기 때문에 Docker hub를 통해 배포하기로 결정했다.
- 이 때, 이미지를 aws linux환경에서 쓸 수 있도록 platform을 설정해야한다.
$ docker build -t <계정이름>/user-container --platform linux/amd64 -f docker-file/Dockerfile.production.user .
$ docker build -t <계정이름>/ecode-backend --platform linux/amd64 -f docker-file/Dockerfile.production .
$ docker login -u <계정이름>
$ docker push <계정이름>/ecode-backend
$ docker push <계정이름>/user-container
여러 플랫폼에서 사용할 수 있는 multi-platform build를 위한 도구인 buildx가 있지만, 지금은 로컬에서 따로 사용하는 이미지가 있기 때문에 생략한다.
EC2에서 실행 테스트
현재 Docker hub로 사용할 모든 이미지를 올려두었기 때문에, 이미지를 받아와 실행할 docker compose 파일과, 환경변수들만 옮겨서 테스트 서버를 띄워볼 수 있다.
EC2 인스턴스 생성 및 접속
EC2 인스턴스 생성
key pair 추가
보안그룹 추가
- ssh
- http
EC2 접속
- ssh 포트 열여있는지 확인
- 생성 시 다운받은 private key를 이용해서 ssh 접속
- AWS 이미지로 만든 ec2의 경우 기본 사용자 계정 이름이 ec2-user임
- private key의 권한이 0400이여야함
$ chmod 0400 login.pem
$ ssh -i login.pem ec2-user@[ec2 ip 주소]
이런식으로 접속할 수 있다.
Docker, Docker compose 설치
Docker 설치과정
패키지 업데이트
$ sudo yum update -y
Docker community edition 설치
$ sudo yum install -y docker
Docker 데몬 시작
$ sudo service docker start
버전 확인
$ docker -v
$ docker ps <- 권한이 안돼서 퍼미션 에러
$ sudo docker ps
docker 명령어 권한 수정(ec2-user도 docker 실행 가능하도록)
$ sudo usermod -a -G docker ec2-user
ssh 종료, 재실행
$ docker ps
재부팅 시 자동실행 설정
$ sudo systemctl enable docker
Docker compose 설치과정
# 플러그인을 저장할 디렉토리 생성
$ mkdir -p ~/.docker/cli-plugins/
# 공식 GitHub 저장소에서 최신 버전의 Docker Compose 다운로드
$ curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose
# 다운로드한 파일에 실행 권한 부여
$ chmod +x ~/.docker/cli-plugins/docker-compose
$ docker compose version

해당 폴더 경로에 release파일을 다운받아서 넣으면 된다는 뜻!
docker-compose 파일과 환경변수 파일 가져오기
- 직접 파일을 서버로 옮기거나, github에서 받아온다.
- github에서 받아올 때, 전체 코드가 아닌 필요한 코드만 받기 위해
sparse-checkout 사용
$ git clone --no-checkout <git 주소>
$ cd <프로젝트 폴더>
$ git sparse-checkout init --cone
$ git sparse-checkout set <원하는 폴더 경로>
$ git sparse-checkout list
$ git checkout main
위 명령어를 통해 docker compose 파일과, docker-env 폴더만 따로 가져왔다.
Docker image pull, 실행
$ docker compose -f docker-compose.production.yml pull
$ docker compose -f docker-compose.production.yml up -d
$ docker ps

브라우저에서 health check
- EC2 보안그룹에 8000번 포트 열려있는지 확인
- EC2 ip주소나 도메인 주소로 접속시도, 이 때 https가 아닌 http로 접속해서 확인

로컬 프론트엔드에서 ec2로 접속시도해보기
- html meta태그에 아래 내용이 있으면 https로 접속을 시도하기 때문에, ssl 설정 전에는 주석처리
<!-- <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"> -->
- cors origin 리스트에 localhost 추가하기(개발 환경에서만)
SSL 접속 설정
- 프론트엔드 배포 상황에서는 vercel의 https 웹페이지에서 http인 백엔드 서버로 요청이 거부된다. (blocked:mixed-content error)
- 따라서 백엔드 서버에도 ssl 설정을 해줘야한다.
도메인 연결
EC2 고정 ip 설정
기본적으로 EC2는 재부팅할 때마다 ip가 바뀌기 때문에 SSL 설정을 위해 고정 ip로 변경해야 한다.
aws ec2에서 network & security에 Elastic IPs에서 하나 할당받고, ec2 인스턴스와 연결해준다.

Route53, gabia 네임서버 설정
- 기존에 gabia DNS서버에 프론트엔드 배포를 위해 vercel DNS 서버들을 추가했었다.
- 하지만 백엔드도 같은 도메인으로 이용하기 위해 네임서버를 vercel이 아닌 AWS Route53으로 전면 이주하는 과정이 필요했다.
Vercel과 AWS Route53을 동시에 이용해서는 안된다. 모든 요청 시 같은 ip주소를 보내줘야하기 때문에 subdomain이 다르더라도 같은 DNS서버로 요청을 보내야한다. 만약 섞여있다면 ip가 달라져서 SSL 설정도 못하고 요청이 실패할 수도 있다.

따라서 이처럼 전부 aws dns 서버로 설정했다.
- 이렇게 DNS서버를 전면 수정하면서 frontend 서버 레코드도 전부 Route53에 다시 추가해줘야 했다.

이렇게 route53을 전부 설정해줬으면 다음 내용들이 잘 실행되는지 체크해보면 좋다.
- Vercel로 가서 frontend 웹페이지들이 잘 뜨는지 확인
- api 서버 주소로 ping을 날려서 발급받은 고정 ip주소로 잘 보내지는지 확인
- 기존에 다른 ip로 연결해서 캐시가 남아있다면 시스템 dns 캐시 지우기
- 브라우저 캐시도 남아있을 수 있어서 시크릿 모드로 GET 요청 날려보기
- dns checker로 도메인이 고정 ip 주소로 잘 인식되는지 확인 dns checker
Certbot 설치
certbot이란 무료 SSL 인증서를 발급받을 수 있는 프로그램이며, 공식문서를 통해 설치를 진행했다.
$ sudo dnf install python3 python-devel augeas-devel gcc
$ sudo python3 -m venv /opt/certbot/
$ sudo /opt/certbot/bin/pip install --upgrade pip
$ sudo /opt/certbot/bin/pip install certbot certbot-nginx
$ sudo ln -s /opt/certbot/bin/certbot /usr/local/bin/certbot
nginx 설치, 설정파일 세팅
- 원래 8000번 포트로 열려있었지만, 80번 포트로 연결해도 자동으로 8000번 포트로 리다이랙트 해줄 수 있도록 nginx 리버스 프록시 설정. EC2 보안그룹에서 8000번 포트 빼도 됨.
$ sudo yum update -y
$ sudo yum install nginx -y
$ sudo systemctl start nginx
$ sudo systemctl enable nginx
$ sudo vim /etc/nginx/conf.d/my_app.conf
%% /etc/nginx/conf.d/my_app.conf %%
server {
listen 80;
server_name <도메인 이름>;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
nginx설정 후 반드시 restart로 적용해줘야함
$ sudo systemctl restart nginx
nginx 설정이 끝났다면 http://<백엔드 도메인>로 브라우저에서 먼저 테스트를 해보고 접속 성공하면 다음으로 진행. 이 때 굳이 :8000처럼 포트를 설정하지 않아도 된다.
SSL 적용
sudo certbot --nginx -d <백엔드 도메인>
해당 명령어를 입력하면 자동으로 SSL 인증서를 발급하고, 성공하면 Success 메시지가 뜬다. 해당 인증서의 유효기간은 3개월정도 지속되며, 만료되면 갱신해줘야한다.
이 때, 자동으로 nginx 설정을 수정해서 443 포트 관련 내용을 추가해주며,
EC2 인스턴스의 보안그룹에도 마찬가지로 https가 허용되어있어야한다.
이제 https://<백엔드 도메인>로 접속하면 잘 적용됨을 확인할 수 있다.
참고로 3개월마다 cron job으로 자동으로 인증서를 생성해주는 방식으로 계속 사용할 수 있다.
Vercel 환경에서 테스트
이제 https로 백엔드 api를 호출할 수 있기 때문에, frontend 배포환경에 백엔드 주소 환경변수를 설정해 준 뒤
새로 배포해준다. 이후 api를 호출해 데이터를 잘 받아오는지 확인한다.